/*
 * KNWebGLParticleObjects.js
 * Keynote HTML Player
 *
 * Created by Tungwei Cheng
 * Copyright (c) 2016-2018 Apple Inc. All rights reserved.
 */

var kNumCameraShakePoints = 10;
var kParticleSize = 16;

var smokeImage = new Image();
smokeImage.src = "";

var speckImage = new Image();
speckImage.src = "";

var flameImage = new Image();
flameImage.src = "";

var fireworksImage = new Image();
fireworksImage.src = "";

var fireworksCenterBurstImage = new Image();
fireworksCenterBurstImage.src = "";

var shimmerImage = new Image();
shimmerImage.src = "";

var sparkleImage = new Image();
sparkleImage.src = "";

var KNWebGLParticleSystem = Class.create({
    initialize: function(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture) {
        this.renderer = renderer;
        this.program = program;

        var gl = this.gl = this.renderer.gl;
        var uniforms = this.uniforms = program.uniforms;
        var attribs = this.attribs = program.attribs;

        // enables attribs and uniforms
        KNWebGLUtil.enableAttribs(gl, program)

        this.texture = texture;
        this.objectSize = objectSize;
        this.slideSize = slideSize;
        this.duration = duration;
        this.particleSize = particleSize;
        this.particleSystemSize = particleSystemSize;
        this.particleCount = particleSystemSize.width * particleSystemSize.height;
        this.particlesWide = particleSystemSize.width;
        this.particlesHigh = particleSystemSize.height;

        // array to store individual particle visibility within the particle system
        this.particleSystemVisibilities = [];

        // set to true by default
        this.shouldDraw = true;

        this.percentfinished = 0.0;
    },

    animationWillBeginWithContext: function() {
        var gl = this.gl;
        var uniforms = this.uniforms;
        var attribs = this.attribs;

        if (uniforms["SpeedMax"] !== undefined) {
            this._speedMax = this.speedMax();
            gl.uniform1f(uniforms["SpeedMax"], this._speedMax);
        }

        if (uniforms["RotationMax"] !== undefined) {
            this._rotationMax = this.rotationMax();
            gl.uniform1f(uniforms["RotationMax"], this._rotationMax);
        }

        if (uniforms["Duration"] !== undefined) {
            this._duration = this.duration / 1000;
            gl.uniform1f(uniforms["Duration"], this._duration);
        }

        var particleSize = this.particleSize;
        var objectSize = this.objectSize;
        var objectBoundsRect = CGRectMake(0, 0, objectSize.width, objectSize.height);
        var texWidth = objectSize.width;
        var texHeight = objectSize.height;
        var pixels;

        if (this.pixels) {
            // create framebuffer to read gl texture
            var fb = gl.createFramebuffer();
            pixels = new Uint8Array(texWidth * texHeight * 4);

            // bind the framebuffer for reading pixels
            gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);

            if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE) {
                gl.readPixels(0, 0, texWidth, texHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
            }

            gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        }

        var isLookingAtObjectTexture = attribs["TexCoord"] !== undefined;

        gl.useProgram(this.program.shaderProgram);

        var attributeBuffers = this.attributeBuffers = {};
        attributeBuffers["Position"] = [];
        attributeBuffers["Center"] = [];
        attributeBuffers["TexCoord"] = [];
        attributeBuffers["ParticleTexCoord"] = [];
        attributeBuffers["Speed"] = [];
        attributeBuffers["Rotation"] = [];
        attributeBuffers["Scale"] = [];
        attributeBuffers["LifeSpan"] = [];
        attributeBuffers["Color"] = [];

        var numberOfVerticesPerParticle = this.numberOfVerticesPerParticle;
        for (var y = 0, particlesHigh = this.particlesHigh; y < particlesHigh; y++) {
            for (var x = 0, particlesWide = this.particlesWide; x < particlesWide; x++) {
                var indexPoint = WebGraphics.makePoint(x, y);
                var index = this.particlesWide * indexPoint.y + indexPoint.x;
                var vertIndex = index * numberOfVerticesPerParticle;
                var particleRect = CGRectMake(x * particleSize.width, y * particleSize.height, particleSize.width, particleSize.height);

                if (isLookingAtObjectTexture && (x === particlesWide - 1 || y === particlesHigh - 1)) {
                    // Make sure we clip last row/column to actual bounds of object
                    particleRect = CGRectIntersection(particleRect, objectBoundsRect);
                }

                var pixelParticleRect = particleRect;

                var visible = true;
                if (this.pixels) {
                    var bytesPerRow = texWidth * 4;

                    // add up color values in region
                    var addingColor = WebGraphics.makePoint4D(0, 0, 0, 0);
                    var attenuationCounter = 0;

                    var maxXX = pixelParticleRect.origin.x + pixelParticleRect.size.width;
                    var maxYY = pixelParticleRect.origin.y + pixelParticleRect.size.height;

                    for (var yy = pixelParticleRect.origin.y; yy < maxYY; ++yy) {
                        for (var xx = pixelParticleRect.origin.x; xx < maxXX; ++xx) {
                            if (xx < texWidth && yy < texHeight) {
                                // find pixel index from top because the object is flipped
                                var index = (texHeight - yy - 1) * texWidth * 4 + xx * 4;

                                // Make sure we're within the OpenGL texture bounds
                                // (our total size can be a little bigger than the actual texture)
                                if (attribs["Color"] !== undefined) {
                                    // attenuate color value by distance from center
                                    var attenuation = 1.0;
                                    if (pixelParticleRect.size.width > 1) {
                                        var xPos = 2 * (xx - pixelParticleRect.origin.x) / pixelParticleRect.size.width - 1;
                                        var yPos = 2 * (yy - pixelParticleRect.origin.y) / pixelParticleRect.size.height - 1;
                                        attenuation = (1 - xPos * xPos) + (1 - yPos * yPos);
                                    }

                                    var r = pixels[index];
                                    var g = pixels[index + 1];
                                    var b = pixels[index + 2];
                                    var a = pixels[index + 3];
                                    var thisPixel = WebGraphics.makePoint4D(r/255, g/255, b/255, a/255);

                                    if (thisPixel.w !== 0.0) {
                                        addingColor.x += attenuation * thisPixel.x / thisPixel.w;
                                        addingColor.y += attenuation * thisPixel.y / thisPixel.w;
                                        addingColor.z += attenuation * thisPixel.z / thisPixel.w;
                                        addingColor.w += attenuation * thisPixel.w;
                                        attenuationCounter += attenuation;
                                    }
                                } else {
                                    // only care about alpha
                                    var a = pixels[index + 3] / 255;

                                    addingColor.w += a;
                                }
                            }
                        }
                    }

                    // set visibility for the particle
                    visible = addingColor.w > 0;
                    this.particleSystemVisibilities.push(visible);

                    if (attribs["Color"] !== undefined) {
                        // set the color
                        if (attenuationCounter == 0) {
                            // If we never saw any pixels, we're just copying all zeroes into color
                            attenuationCounter = 1;
                        }

                        var invAttenuationCounter = 1.0 / attenuationCounter;
                        var theColor = WebGraphics.multiplyPoint4DByScalar(addingColor, invAttenuationCounter)
                        var attributeBuffersColor = attributeBuffers["Color"];

                        KNWebGLUtil.setPoint4DAtIndexForAttribute(theColor, vertIndex, attributeBuffersColor);
                        KNWebGLUtil.setPoint4DAtIndexForAttribute(theColor, vertIndex + 1, attributeBuffersColor);
                        KNWebGLUtil.setPoint4DAtIndexForAttribute(theColor, vertIndex + 2, attributeBuffersColor);
                        KNWebGLUtil.setPoint4DAtIndexForAttribute(theColor, vertIndex + 3, attributeBuffersColor);
                    }
                }

                if (this.willOverrideStartingPoints) {
                    particleRect.origin =  WebGraphics.setOrigin(particleRect.origin, this.startingPointAtIndexPoint(indexPoint));
                }

                var vertices = [];

                vertices[0] = WebGraphics.makePoint(particleRect.origin.x, particleRect.origin.y);
                if (numberOfVerticesPerParticle > 1) {
                    vertices[1] = WebGraphics.makePoint(particleRect.origin.x + particleRect.size.width, particleRect.origin.y);
                    vertices[2] = WebGraphics.makePoint(particleRect.origin.x + particleRect.size.width, particleRect.origin.y + particleRect.size.height);
                    vertices[3] = WebGraphics.makePoint(particleRect.origin.x, particleRect.origin.y + particleRect.size.height);
                }

                for (var i = 0; i < numberOfVerticesPerParticle; i++) {
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(vertices[i], vertIndex + i, attributeBuffers["Position"]);
                }

                if (attribs["Center"] !== undefined) {
                    var midX = particleRect.origin.x + (particleRect.size.width / 2);
                    var midY = particleRect.origin.y + (particleRect.size.height / 2);

                    KNWebGLUtil.setPoint2DAtIndexForAttribute(
                        WebGraphics.makePoint(midX, midY), vertIndex, attributeBuffers["Center"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(
                        WebGraphics.makePoint(midX, midY), vertIndex + 1, attributeBuffers["Center"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(
                        WebGraphics.makePoint(midX, midY), vertIndex + 2, attributeBuffers["Center"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(
                        WebGraphics.makePoint(midX, midY), vertIndex + 3, attributeBuffers["Center"]);
                }

                if (attribs["TexCoord"] !== undefined) {
                    for (var i = 0; i < numberOfVerticesPerParticle; i++) {
                        var texCoord = WebGraphics.makePoint(vertices[i].x / objectSize.width, vertices[i].y / objectSize.height);
                        KNWebGLUtil.setPoint2DAtIndexForAttribute(texCoord, vertIndex + i, attributeBuffers["TexCoord"]);
                    }
                }

                if (attribs["ParticleTexCoord"] !== undefined) {
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(
                        WebGraphics.makePoint(0, 0), vertIndex, attributeBuffers["ParticleTexCoord"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(
                        WebGraphics.makePoint(1, 0), vertIndex + 1, attributeBuffers["ParticleTexCoord"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(
                        WebGraphics.makePoint(1, 1), vertIndex + 2, attributeBuffers["ParticleTexCoord"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(
                        WebGraphics.makePoint(0, 1), vertIndex + 3, attributeBuffers["ParticleTexCoord"]);
                }

                if (attribs["Speed"] !== undefined) {
                    var speed = this.speedAtIndexPoint(indexPoint);

                    KNWebGLUtil.setPoint3DAtIndexForAttribute(speed, vertIndex, attributeBuffers["Speed"]);
                    KNWebGLUtil.setPoint3DAtIndexForAttribute(speed, vertIndex + 1, attributeBuffers["Speed"]);
                    KNWebGLUtil.setPoint3DAtIndexForAttribute(speed, vertIndex + 2, attributeBuffers["Speed"]);
                    KNWebGLUtil.setPoint3DAtIndexForAttribute(speed, vertIndex + 3, attributeBuffers["Speed"]);
                }

                if (attribs["Rotation"] !== undefined) {
                    var rotation = this.rotationAtIndexPoint(indexPoint);

                    KNWebGLUtil.setPoint3DAtIndexForAttribute(rotation, vertIndex, attributeBuffers["Rotation"]);
                    KNWebGLUtil.setPoint3DAtIndexForAttribute(rotation, vertIndex + 1, attributeBuffers["Rotation"]);
                    KNWebGLUtil.setPoint3DAtIndexForAttribute(rotation, vertIndex + 2, attributeBuffers["Rotation"]);
                    KNWebGLUtil.setPoint3DAtIndexForAttribute(rotation, vertIndex + 3, attributeBuffers["Rotation"]);
                }

                if (attribs["Scale"] !== undefined) {
                    var scale = this.scaleAtIndexPoint(indexPoint);

                    KNWebGLUtil.setFloatAtIndexForAttribute(scale, vertIndex, attributeBuffers["Scale"]);
                    KNWebGLUtil.setFloatAtIndexForAttribute(scale, vertIndex + 1, attributeBuffers["Scale"]);
                    KNWebGLUtil.setFloatAtIndexForAttribute(scale, vertIndex + 2, attributeBuffers["Scale"]);
                    KNWebGLUtil.setFloatAtIndexForAttribute(scale, vertIndex + 3, attributeBuffers["Scale"]);
                }

                if (attribs["LifeSpan"] !== undefined) {
                    var lifeSpan = this.lifeSpanAtIndexPoint(indexPoint);

                    KNWebGLUtil.setPoint2DAtIndexForAttribute(lifeSpan, vertIndex, attributeBuffers["LifeSpan"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(lifeSpan, vertIndex + 1, attributeBuffers["LifeSpan"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(lifeSpan, vertIndex + 2, attributeBuffers["LifeSpan"]);
                    KNWebGLUtil.setPoint2DAtIndexForAttribute(lifeSpan, vertIndex + 3, attributeBuffers["LifeSpan"]);
                }

                if (attribs["Color"] !== undefined && this.willOverrideColors) {
                    var color = this.colorAtIndexPoint(indexPoint);

                    KNWebGLUtil.setPoint4DAtIndexForAttribute(color, vertIndex, attributeBuffers["Color"]);
                    KNWebGLUtil.setPoint4DAtIndexForAttribute(color, vertIndex + 1, attributeBuffers["Color"]);
                    KNWebGLUtil.setPoint4DAtIndexForAttribute(color, vertIndex + 2, attributeBuffers["Color"]);
                    KNWebGLUtil.setPoint4DAtIndexForAttribute(color, vertIndex + 3, attributeBuffers["Color"]);
                }
            }
        }

        var indexCounter = 0;
        var elementArray = this.elementArray = [];
        for (var i = 0; i < this.particleCount; i++) {
            elementArray[indexCounter++] = 4 * i + 0;
            elementArray[indexCounter++] = 4 * i + 1;
            elementArray[indexCounter++] = 4 * i + 2;

            // second triangle
            elementArray[indexCounter++] = 4 * i + 0;
            elementArray[indexCounter++] = 4 * i + 2;
            elementArray[indexCounter++] = 4 * i + 3;
        }

        this.buffer = {};
        KNWebGLUtil.bindAllAvailableAttributesToBuffers(gl, attribs, attributeBuffers, {
            "LifeSpan": 2,
            "Scale": 1,
            "Rotation": 3,
            "Speed": 3,
            "ParticleTexCoord": 2,
            "TexCoord": 2,
            "Center": 2,
            "Position": 2,
            "Color": 4
        }, this.buffer,
        gl.DYNAMIC_DRAW);

        this.elementArrayBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.elementArrayBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(elementArray), gl.DYNAMIC_DRAW);

        if (this.uniforms["Texture"]) {
            gl.uniform1i(this.uniforms["Texture"], 0);
        }

        if (this.uniforms["ParticleTexture"]) {
            gl.uniform1i(this.uniforms["ParticleTexture"], 0);
        }
    },

    drawFrame: function(percent, opacity) {
        var gl = this.gl;
        var program = this.program;
        var uniforms = this.uniforms;

        this.percentfinished = percent;

        gl.useProgram(program.shaderProgram);
        gl.bindTexture(gl.TEXTURE_2D, this.texture);
        gl.uniform1f(uniforms["Percent"], percent);
        gl.uniform1f(uniforms["Opacity"], opacity);

        KNWebGLUtil.bindAllAvailableAttributesToBuffers(gl, this.attribs, this.attributeBuffers, {
            "LifeSpan": 2,
            "Scale": 1,
            "Rotation": 3,
            "Speed": 3,
            "ParticleTexCoord": 2,
            "TexCoord": 2,
            "Center": 2,
            "Position": 2,
            "Color": 4
        }, this.buffer,
        gl.DYNAMIC_DRAW);

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.elementArrayBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.elementArray), gl.DYNAMIC_DRAW);

        this.draw();
    },

    draw: function() {
        var gl = this.gl;
        gl.drawElements(gl.TRIANGLES, this.elementArray.length, gl.UNSIGNED_SHORT, 0);
    },

    setColor: function(color) {
        var gl = this.gl;
        gl.useProgram(this.program.shaderProgram);
        gl.uniform4fv(this.uniforms["Color"], color);
    },

    setMVPMatrix: function(MVPMatrix) {
        var gl = this.gl;
        gl.useProgram(this.program.shaderProgram);
        gl.uniformMatrix4fv(this.uniforms["MVPMatrix"], false, MVPMatrix);
    },

    indexFromPoint: function(point) {
        return this.particlesWide * point.y + point.x;
    },

    particleSystemSizeWithRequestedNumber: function(requestedNumParticles, objectSize) {
        var maxNumParticles = 65535 / this.numberOfVerticesPerParticle;
        requestedNumParticles = Math.min(requestedNumParticles, maxNumParticles);

        if ((objectSize.width === 0 && objectSize.height === 0) || this.willOverrideStartingPoints || requestedNumParticles === 1) {
            return WebGraphics.makeSize(requestedNumParticles, 1);
        }

        if (requestedNumParticles >= objectSize.width * objectSize.height) {
            return objectSize;
        }

        if (requestedNumParticles < 1) {
            requestedNumParticles = 1;
            return WebGraphics.makeSize(requestedNumParticles, 1);
        }

        var currentNumParticles = 0, prevNumParticles = 0;
        var particleSize = Math.round(Math.sqrt(objectSize.width * objectSize.height));

        //find best fit for number of particles
        currentNumParticles = Math.ceil(objectSize.width / particleSize) * Math.ceil(objectSize.height / particleSize);

        if (currentNumParticles === requestedNumParticles) {
            return WebGraphics.makeSize(particleSize, particleSize);
        }

        if (currentNumParticles < requestedNumParticles) {
            do {
                prevNumParticles = currentNumParticles;
                particleSize--;
                currentNumParticles = Math.ceil(objectSize.width/particleSize) * Math.ceil(objectSize.height/particleSize);
            } while (currentNumParticles < requestedNumParticles && particleSize > 2);

            if (particleSize <= 2.0) {
                return WebGraphics.makeSize(Math.ceil(objectSize.width / 2), Math.ceil(objectSize.height / 2));
            }

            if (Math.abs(currentNumParticles - requestedNumParticles) < Math.abs(prevNumParticles - requestedNumParticles)) {
                return WebGraphics.makeSize(Math.ceil(objectSize.width / particleSize), Math.ceil(objectSize.height / particleSize));
            } else {
                return WebGraphics.makeSize(Math.ceil(objectSize.width / (particleSize + 1)), Math.ceil(objectSize.height / (particleSize + 1)));
            }
        } else {
            do {
                prevNumParticles = currentNumParticles;
                particleSize++;
                currentNumParticles = Math.ceil(objectSize.width / particleSize) * Math.ceil(objectSize.height / particleSize);
            } while (currentNumParticles > requestedNumParticles && particleSize > 2);

            if (particleSize <= 2.0) {
                return WebGraphics.makeSize(Math.ceil(objectSize.width / 2), Math.ceil(objectSize.height / 2));
            }

            if (Math.abs(currentNumParticles - requestedNumParticles) < Math.abs(prevNumParticles - requestedNumParticles)) {
                return WebGraphics.makeSize(Math.ceil(objectSize.width / particleSize), Math.ceil(objectSize.height / particleSize));
            } else {
                return WebGraphics.makeSize(Math.ceil(objectSize.width / (particleSize + 1)), Math.ceil(objectSize.height / (particleSize + 1)));
            }
        }
    },

    point3DRandomDirection: function() {
        var u = WebGraphics.randomBetween(-1.0, 1.0);
        var theta = WebGraphics.randomBetween(0, 2.0 * Math.PI);
        var prefix = Math.sqrt(1.0 - u * u);
        var result = WebGraphics.makePoint3D(prefix * Math.cos(theta), prefix * Math.sin(theta), u);

        return result;
    }
});

var KNWebGLBuildAnvilSmokeSystem = Class.create(KNWebGLParticleSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture) {
        this.willOverrideStartingPoints = true;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture);

        this.setupWithTexture();
    },

    setupWithTexture: function() {
        this.animationWillBeginWithContext();
    },

    startingPointAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint);
        return WebGraphics.makePoint(index / this.particleCount * this.objectSize.width - kParticleSize / 2, kParticleSize / 2);
    },

    speedAtIndexPoint: function(point) {
        var xDirection = (2 * this.indexFromPoint(point)) / (this.particleCount) -1;
        xDirection = (xDirection < 0 ? -1 : 1) * Math.sqrt(Math.abs(xDirection));
        //we've adjusted these values specifically for WebGL
        var yDirection = (Math.abs(xDirection) + 0.25) * WebGraphics.randomBetween(-1.0, 0.1);
        xDirection *= WebGraphics.randomBetween(0.25, 1);
        var speed = WebGraphics.makePoint3D(
            this.p_anvilGlobalScale() * 5.6 * xDirection,
            this.p_anvilGlobalScale() * 5 * -yDirection, 0);
        return speed;
    },

    rotationAtIndexPoint: function(point) {
        var xDirection = (2 * this.indexFromPoint(point)) / (this.particleCount) - 1;
        var thisRotation = xDirection * WebGraphics.randomBetween(0.5, 1) * Math.PI;
        thisRotation *= this.duration / 1000;
        return WebGraphics.makePoint3D(0, 0, thisRotation);
    },

    scaleAtIndexPoint: function(indexPoint) {
        var scale = Math.abs(((2.0 * this.indexFromPoint(indexPoint)) / this.particleCount) - 1.0);
        scale += 1.25;
        var randVal = WebGraphics.randomBetween(0, 1);
        randVal *= randVal * randVal;
        scale *= WebGraphics.mix(1, 2.5, randVal);
        scale *= (this.p_anvilGlobalScale() / kParticleSize);
        return scale;
    },

    lifeSpanAtIndexPoint: function(indexPoint) {
        return WebGraphics.makePoint(0, (this.duration / 1000) * WebGraphics.randomBetween(0.15, 1));
    },

    p_anvilGlobalScale: function() {
        return WebGraphics.mix(1.25, 0.75, this.objectSize.width / this.slideSize.width) * (this.objectSize.width/7);
    }
});

var KNWebGLBuildAnvilSpeckSystem = Class.create(KNWebGLParticleSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture) {
        this.willOverrideStartingPoints = true;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture);

        this.setupWithTexture();
    },

    setupWithTexture: function() {
        this.animationWillBeginWithContext();
    },

    startingPointAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint);
        return WebGraphics.makePoint(index/this.particleCount * this.objectSize.width - kParticleSize/2, kParticleSize/2);
    },

    speedAtIndexPoint: function(point) {
        var speed = WebGraphics.makePoint3D(0, 0, 0);
        var index = this.indexFromPoint(point);
        speed.x = 2 * index / this.particleCount - 1;
        speed.y = WebGraphics.randomBetween(0.2, 1);
        if (index % 3 != 1) {
            speed.z = 0.2;
            speed.y *= 0.025 * (WebGraphics.randomBetween(0,1) < 0.5 ? -1 : 1);
            speed.x *= 10;
            speed.y *= 10;
        } else {
            speed.z = 1.0;
        }
        speed.x *= this.p_anvilGlobalScale() * 5.25;
        speed.y *= this.p_anvilGlobalScale() * 4; //only difference is our coordinate system is setup reversed to the desktop, so we need to multiply by -4 instead of 4
        return speed;
    },

    rotationAtIndexPoint: function(point) {
        var xDirection = (2 * this.indexFromPoint(point)) / (this.particleCount) - 1;
        var thisRotation = xDirection * WebGraphics.randomBetween(0.5, 1) * Math.PI;
        thisRotation *= this.duration / 1000;
        return WebGraphics.makePoint3D(0, 0, thisRotation);
    },

    scaleAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint);
        var scale = WebGraphics.randomBetween(3, 5);
        if (index % 3 != 1) {
            scale *= 3;
        }
        scale *= (this.p_anvilGlobalScale() / kParticleSize);
        return scale;
    },

    lifeSpanAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint);
        var lifeSpan = WebGraphics.makePoint(0, Math.min(1, this.duration/1000) * WebGraphics.randomBetween(0.2, 0.5));
        if (index % 3 != 1) {
            lifeSpan.y *= 10;
        }
        return lifeSpan;
    },

    p_anvilGlobalScale: function() {
        return WebGraphics.mix(1.25, 0.75, this.objectSize.width / this.slideSize.width)*(this.objectSize.width/7);
    }
});

var KNWebGLBuildFlameSystem = Class.create(KNWebGLParticleSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, numParticles, texture) {
        // overwrite starting point
        this.willOverrideStartingPoints = true;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        var width = objectSize.width;
        var height = objectSize.height;
        var particleSystemSize = this.particleSystemSizeWithRequestedNumber(numParticles, WebGraphics.makeSize(width, height));
        var particleSize = WebGraphics.makeSize(Math.ceil(width / particleSystemSize.width), Math.ceil(height / particleSystemSize.height));

        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture);
    },

    p_setupParticleDataWithTexture: function(textureInfo) {
        var gl = this.gl;
        var width = textureInfo.width;
        var height = textureInfo.height;

        // create framebuffer to read gl texture
        var fb = gl.createFramebuffer();
        var pixels = new Uint8Array(width * height * 4);

        // bind the framebuffer for reading pixels
        gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureInfo.texture, 0);

        if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE) {
          gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
        }

        var minPoint = {
            "x": width,
            "y": height
        };

        var maxPoint = {
            "x": 0,
            "y": 0
        };

        // Want to start the flames at the alpha-bottom of the image, not the absolute bottom
        var bottomRow = [];
        var bottomRowCount = width;
        var actualSize = {};

        for (var i = 0; i < width * 4; i += 4) {
            var x = i / 4;
            var minY = Number.MAX_VALUE;
            var maxY = 0;
            var hasNonTransparentPixels = false;
            var thisPoint = {
                "x": x / width,
                "y": -1
            };

            // search from top because the object is flipped
            for (var j = height - 1; j >= 0; j--) {
                var alphaIndex = x * 4 + j * (width) * 4 + 3;
                var y = height - j;

                if (pixels[alphaIndex]/255 > 0.1) {
                    minY = Math.min(y, minY);
                    maxY = Math.max(y, maxY);
                    hasNonTransparentPixels = true;
                }
            }

            if (hasNonTransparentPixels) {
                thisPoint.y = maxY / height;

                minPoint.x = Math.min(minPoint.x, x);
                maxPoint.x = Math.max(maxPoint.x, x);
                minPoint.y = Math.min(minPoint.y, minY);
                maxPoint.y = Math.max(maxPoint.y, maxY);
            }

            bottomRow[x] = thisPoint;
        }

        actualSize = {
            width: maxPoint.x - minPoint.x,
            height: maxPoint.y - minPoint.y
        };

        this._actualSize = actualSize;
        this._bottomRow = bottomRow;
        this._bottomRowCount = bottomRowCount;

        // unbind the framebuffer
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        this.animationWillBeginWithContext();

        var positionBuffer = this.attributeBuffers["Position"];
        var centerBuffer = this.attributeBuffers["Center"];

        // adjust vertices according to starting scale
        var minSide = this._actualSize.height / 2.0;
        var numberOfVerticesPerParticle = this.numberOfVerticesPerParticle;

        for (var index = 0, length = this.particleCount; index < length; index++) {
            var vertIndex = index * numberOfVerticesPerParticle;
            var origin = {
                "x": positionBuffer[vertIndex*2],
                "y": positionBuffer[vertIndex*2 + 1]
            };

            origin.x -= minSide / 2;
            origin.y -= minSide / 2;

            var newRect = WebGraphics.makeRect(origin.x, origin.y, minSide, minSide);
            var center = WebGraphics.makePoint(origin.x + minSide/2,  origin.y + minSide/2);

            KNWebGLUtil.setPoint2DAtIndexForAttribute(WebGraphics.makePoint(newRect.x, newRect.y), vertIndex, positionBuffer);
            KNWebGLUtil.setPoint2DAtIndexForAttribute(WebGraphics.makePoint(newRect.x + newRect.width, newRect.y), vertIndex + 1, positionBuffer);
            KNWebGLUtil.setPoint2DAtIndexForAttribute(WebGraphics.makePoint(newRect.x + newRect.width, newRect.y + newRect.height), vertIndex + 2, positionBuffer);
            KNWebGLUtil.setPoint2DAtIndexForAttribute(WebGraphics.makePoint(newRect.x, newRect.y + newRect.height), vertIndex + 3, positionBuffer);

            for (var i = 0; i < 4; i++) {
                KNWebGLUtil.setPoint2DAtIndexForAttribute(center, vertIndex + i, centerBuffer);
            }
        }
    },

    startingPointAtIndexPoint: function(indexPoint) {
        var x = 0;
        var y = 0;
        var foundY = false;

        for (var i = 0; i < this._bottomRowCount; i++) {
            // at least one row has y value not equal to -1
            if (this._bottomRow[i].y !== -1) {
                foundY = true;
                break;
            }
        }

        if (this._bottomRow && foundY) {
            do {
                var index = WebGraphics.randomBetween(0, this._bottomRowCount - 1);
                index = Math.round(index);
                x = this._bottomRow[index].x;
                y = this._bottomRow[index].y;
            } while (y === -1);
        }

        var positionPoint = {
            "x": x * this.objectSize.width,
            "y": y * this.objectSize.height
        };
        return positionPoint;
    },

    speedAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint) * 4;
        var yPos = this.attributeBuffers["Position"][index*2 + 1];

        yPos = Math.min(yPos, this._actualSize.height);

        var maxUp = (this._actualSize.height * 0.2 + 0.9 * yPos) * (1.0 + WebGraphics.randomBetween(0.0, 0.1));
        var result = WebGraphics.makePoint3D(0, maxUp, 0);
        result = WebGraphics.multiplyPoint3DByScalar(result, 1.0 / this.speedMax());
        return result;
    },

    speedMax: function() {
        return this._actualSize.height * 1.1 * 1.1;
    },

    lifeSpanAtIndexPoint: function(indexPoint) {
        // CONSTANTS
        var maxParticleLife = Math.min(1.0, 1.0 / Math.max(2, this._duration));
        var thisParticleLife = maxParticleLife * WebGraphics.randomBetween(0.8, 1.0);

        // more at beginning
        var time = this.indexFromPoint(indexPoint) / this.particleCount;

        time = time * time * 0.25 + time * 0.75;
        time *= 1.0 - thisParticleLife;

        var lifeSpan = WebGraphics.makePoint(time, thisParticleLife);
        return lifeSpan;
    },

    rotationAtIndexPoint: function(indexPoint) {
        var rotation = WebGraphics.makePoint3D(WebGraphics.randomBetween(-1.0, 1.0), 0, WebGraphics.randomBetween(-1.0, 1.0));
        return rotation;
    },

    rotationMax: function() {
        return  Math.PI * 2;
    }
});

var KNWebGLBuildConfettiSystem = Class.create(KNWebGLParticleSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, numParticles, texture) {
        this.willOverrideStartingPoints = false;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        var width = objectSize.width;
        var height = objectSize.height;
        var particleSystemSize = this.particleSystemSizeWithRequestedNumber(numParticles, WebGraphics.makeSize(width, height));
        var particleSize = WebGraphics.makeSize(Math.ceil(width / particleSystemSize.width), Math.ceil(height / particleSystemSize.height));

        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture);

        this.setupWithTexture();
    },

    setupWithTexture: function() {
        this.animationWillBeginWithContext();
    },

    startingPointAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint);
        return WebGraphics.makePoint(index / this.particleCount * this.objectSize.width - kParticleSize / 2, kParticleSize / 2);
    },

    speedAtIndexPoint: function(point) {
        var speedMax = 0.025;
        var speedRandMax = speedMax * 20;
        var speedAdjust = this.objectSize.width / this.slideSize.width * this.objectSize.height / this.slideSize.height;
        speedAdjust = Math.sqrt(speedAdjust);
        speedAdjust = 1.0 - speedAdjust * 0.75;
        speedMax *= speedAdjust;
        speedRandMax *= speedAdjust;

        var minSide = Math.min(this.objectSize.height, this.objectSize.width);
        var theRandSpeed = minSide * speedRandMax;
        var theSpeed = minSide * speedMax;
        var maxRadiusSquared = (this.particleSystemSize.width * this.particleSystemSize.width + this.particleSystemSize.height * this.particleSystemSize.height) / 4.0;
        var randSpeed = WebGraphics.makePoint3D(WebGraphics.randomBetween(-1, 1), WebGraphics.randomBetween(-1, 1), WebGraphics.randomBetween(-1, 1));
        randSpeed.z = -Math.abs(randSpeed.z);
        randSpeed = WebGraphics.multiplyPoint3DByScalar(randSpeed, theRandSpeed);

        var vector = WebGraphics.makePoint3D(point.x - this.particlesWide / 2.0, point.y - this.particlesHigh / 2.0, 0);
        var radiusSquared = (vector.x * vector.x + vector.y * vector.y);
        vector.z = Math.sqrt(maxRadiusSquared - radiusSquared);

        var speed = WebGraphics.multiplyPoint3DByScalar(vector, theSpeed);
        speed = WebGraphics.addPoint3DToPoint3D(speed, randSpeed);

        return speed;
    },

    rotationAtIndexPoint: function(point) {
        var rotationMax = 8.0 * Math.PI;
        var rotation = WebGraphics.makePoint3D(WebGraphics.randomBetween(-1, 1), WebGraphics.randomBetween(-1, 1), WebGraphics.randomBetween(-1, 1));

        return WebGraphics.multiplyPoint3DByScalar(rotation, rotationMax);
    },

    scaleAtIndexPoint: function(indexPoint) {
        return 1.0;
    }
});

var KNWebGLBuildDiffuseSystem = Class.create(KNWebGLParticleSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, numParticles, texture, l2r) {
        this.willOverrideStartingPoints = false;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        // set direction
        this.l2r = l2r;

        var width = objectSize.width;
        var height = objectSize.height;
        var particleSystemSize = this.particleSystemSizeWithRequestedNumber(numParticles, WebGraphics.makeSize(width, height));
        var particleSize = WebGraphics.makeSize(Math.ceil(width / particleSystemSize.width), Math.ceil(height / particleSystemSize.height));

        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture);

        this.setupWithTexture();
    },

    setupWithTexture: function() {
        this.animationWillBeginWithContext();
    },

    speedAtIndexPoint: function(indexPoint) {
        var l2r = this.l2r;
        //more random towards center
        var randDirection = WebGraphics.makePoint3D(WebGraphics.randomBetween(-1, 1), WebGraphics.randomBetween(-1, 1), WebGraphics.randomBetween(-1, 1));
        var vertical = 2 * indexPoint.y / this.particleSystemSize.height - 1.0;
        randDirection = WebGraphics.multiplyPoint3DByScalar(randDirection, (1.1 - vertical * vertical) * 8);

        //actual direction based on position of fragment height
        var startDirection = WebGraphics.makePoint3D((2 - Math.abs(vertical)) * (l2r ? -1: 1), vertical * 2, 0);
        startDirection = WebGraphics.multiplyPoint3DByScalar(startDirection, 5.0);

        var theDirection = WebGraphics.addPoint3DToPoint3D(startDirection, randDirection);
        theDirection = WebGraphics.point3DNormalize(theDirection);

        return theDirection;
    },

    speedMax: function() {
        var speed =  this.objectSize.height * 1.5 / Math.sqrt(this.duration / 1000);
        return speed;
    },

    rotationAtIndexPoint: function(indexPoint) {
        var rotation = WebGraphics.makePoint3D(WebGraphics.randomBetween(-1, 1), WebGraphics.randomBetween(-1, 1), 0);
        return rotation;
    },

    rotationMax: function() {
        var rotationMax = 8.0 * Math.PI;
        return rotationMax;
    },

    lifeSpanAtIndexPoint: function(indexPoint) {
        var l2r = this.l2r;
        var width = this.particleSystemSize.width;
        var maxParticleLife = WebGraphics.clamp(0.8 / (this.duration / 1000), 0.1, 0.9);
        var time = (l2r ? (width - indexPoint.x): indexPoint.x) / width;
        time *= (1 - maxParticleLife);

        var lifeSpan = WebGraphics.makePoint(time, maxParticleLife);
        return lifeSpan;
    }
});

var KNWebGLBuildFireworksSystem = Class.create(KNWebGLParticleSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture) {
        // override starting point
        this.willOverrideStartingPoints = true;

        // override colors
        this.willOverrideColors = true;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture);
    },

    setupWithTexture: function(textureInfo) {
        var gl = this.gl;
        var width = textureInfo.width;
        var height = textureInfo.height;

        var val1 = -Math.min(this.objectSize.height, this.slideSize.height / 10);
        var val2 = WebGraphics.randomBetween(val1, this.objectSize.height)

        this._startingPoint = WebGraphics.makePoint(this.fireworkStartingPositionX * this.objectSize.width, val2);

        // Initialize random color
        var startColor = WebGraphics.colorWithHSBA(WebGraphics.randomBetween(0, 1), 1, 1, 1);

        this._startingColorRGB = WebGraphics.makePoint3D(startColor.red, startColor.green, startColor.blue);

        this.animationWillBeginWithContext();
    },

    startingPointAtIndexPoint: function(indexPoint) {
        return this._startingPoint;
    },

    colorAtIndexPoint: function(indexPoint) {
        var randomColor = this.point3DRandomDirection();
        var randomColorResult = WebGraphics.multiplyPoint3DByScalar(randomColor, this.colorRandomness);

        var color = WebGraphics.addPoint3DToPoint3D(this._startingColorRGB, randomColorResult);
        color.x = WebGraphics.clamp(color.x, 0, 1);
        color.y = WebGraphics.clamp(color.y, 0, 1);
        color.z = WebGraphics.clamp(color.z, 0, 1);

        var result = {
            "x": color.x,
            "y": color.y,
            "z": color.z,
            "w": 1
        };

        return result;
    },

    speedAtIndexPoint: function(indexPoint) {
        var speed = this.point3DRandomDirection();
        var randomSpeed = WebGraphics.randomBetween(0.8, 1.0);
        var result = WebGraphics.multiplyPoint3DByScalar(speed, randomSpeed);

        return result;
    },

    speedMax: function() {
        return this.maxDistance;
    },

    scaleAtIndexPoint: function(indexPoint) {
        var scale = WebGraphics.randomBetween(this.randomParticleSizeMinMax.width, this.randomParticleSizeMinMax.height);

        return scale;
    },

    lifeSpanAtIndexPoint: function(indexPoint) {
        var lifeSpan = WebGraphics.makePoint(0, WebGraphics.randomBetween(this.lifeSpanMinDuration, 1.0));

        return lifeSpan;
    }
});

var KNWebGLBuildShimmerSystem = Class.create(KNWebGLParticleSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture) {
        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture);
    },

    speedMax: function() {
        var sqrtArea = Math.sqrt(this.objectSize.width * this.objectSize.height);
        var result = sqrtArea * 0.075;

        return result;
    },

    speedAtIndexPoint: function(indexPoint) {
        var speed = this.point3DRandomDirection();
        speed.z = 0;

        return speed;
    }
});

var KNWebGLBuildShimmerObjectSystem = Class.create(KNWebGLBuildShimmerSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, numParticles, texture, direction) {
        // override starting point
        this.willOverrideStartingPoints = false;

        // override colors
        this.willOverrideColors = false;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        // require setting color pixels
        this.pixels = true;

        // set direction
        this.direction = direction;

        var width = objectSize.width;
        var height = objectSize.height;
        var particleSystemSize = this.particleSystemSizeWithRequestedNumber(numParticles, WebGraphics.makeSize(width, height));
        var particleSize = WebGraphics.makeSize(Math.ceil(width / particleSystemSize.width), Math.ceil(height / particleSystemSize.height));

        // call KNWebGLBuildShimmerSystem initialize method
        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture, direction);

        this.setupWithTexture();
    },

    setupWithTexture: function() {
        this.animationWillBeginWithContext();
    },

    drawGLSLWithPercent: function(percent, particleOpacity, rotation, clockwise) {
        var delayedPercent = Math.max(0.0, percent * 1.1 - 0.1);
        var origTexPercent = 1.0 - Math.min(1.0, TSDMixFloats(5.0 * percent, percent * percent, percent));

        // Rotation Matrix
        var angle = delayedPercent * (clockwise ? 1 : -1) * 2;
        var rotMatrix = CGAffineTransformMakeRotation(angle);
        var mat3 = WebGraphics.makeMat3WithAffineTransform(rotMatrix);

        // set mat3 uniform for RotationMatrix
        this.gl.uniformMatrix3fv(this.uniforms["RotationMatrix"], false, mat3);

        // draw GLSL with percent
        this.drawFrame(delayedPercent, particleOpacity * origTexPercent);
    }
});

var KNWebGLBuildShimmerParticleSystem = Class.create(KNWebGLBuildShimmerSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, objectSystem, texture, direction) {
        // override starting point
        this.willOverrideStartingPoints = true;

        // override colors
        this.willOverrideColors = true;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        // set direction
        this.direction = direction;

        // object system particle count
        this.objectSystem = objectSystem;

        // object system vertex count
        this.objectSystemVertexCount = objectSystem.particleCount * 4;

        var kNumHullPoints = this.kNumHullPoints = 20

        //beginning of width of bottom spire on x-axis
        var kSpireMinW = 0.48828125;

        //beginning of height of bottom spire on y-axis
        var kSpireMinH = 0.02;

        //top of height of bottom spire on y-axis
        var kSpireMaxH = 0.3;

        this.p_particleHullArray = [
            // center quad
            { x: kSpireMaxH, y: kSpireMaxH },
            { x: 1 - kSpireMaxH, y: kSpireMaxH },
            { x: 1 - kSpireMaxH, y: 1 - kSpireMaxH },
            { x: kSpireMaxH, y: 1 - kSpireMaxH },
            // top quad
            { x: kSpireMinW, y: 1 - kSpireMaxH },
            { x: 1 - kSpireMinW, y: 1 - kSpireMaxH },
            { x: 1 - kSpireMinW, y: 1 - kSpireMinH },
            { x: kSpireMinW, y: 1 - kSpireMinH },
            // right quad
            { x: 1 - kSpireMaxH, y: 1 - kSpireMinW },
            { x: 1 - kSpireMinH, y: 1 - kSpireMinW },
            { x: 1 - kSpireMinH, y: kSpireMinW },
            { x: 1 - kSpireMaxH, y: kSpireMinW },
            // bottom quad
            { x: kSpireMinW, y: kSpireMinH },
            { x: 1 - kSpireMinW, y: kSpireMinH },
            { x: 1 - kSpireMinW, y: kSpireMaxH },
            { x: kSpireMinW, y: kSpireMaxH },
            // left quad
            { x: kSpireMinH, y: kSpireMinW },
            { x: kSpireMaxH, y: kSpireMinW },
            { x: kSpireMaxH, y: 1 - kSpireMinW },
            { x: kSpireMinH, y: 1 - kSpireMinW },
        ];

        // call KNWebGLBuildShimmerSystem initialize method
        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture, direction);

        this.setupWithTexture();
    },

    setupWithTexture: function() {
        this.animationWillBeginWithContext();

        // update vertex data
        this.p_setupVertexData();
    },

    p_setupVertexData: function() {
        var texRect = CGRectMake(0, 0, 1, 1);
        var numVertsPP = this.numberOfVerticesPerParticle;

        var oldParticleCount = this.particleCount;
        var newParticleCount = this.particleCount * 5;

        // objtain a reference of current data buffer
        var oldAttributeBuffers = this.attributeBuffers;

        // Set up fresh, new data buffer attributes!
        var newAttributeBuffers = {};
        newAttributeBuffers["Position"] = [];
        newAttributeBuffers["Center"] = [];
        newAttributeBuffers["TexCoord"] = [];
        newAttributeBuffers["ParticleTexCoord"] = [];
        newAttributeBuffers["Speed"] = [];
        newAttributeBuffers["Rotation"] = [];
        newAttributeBuffers["Scale"] = [];
        newAttributeBuffers["LifeSpan"] = [];
        newAttributeBuffers["Color"] = [];

        // Update new buffer with old buffer values
        for (var i = 0; i < oldParticleCount; ++i) {
            var oldVertIndex = i * 4;

            var thisScale = this.attributeBuffers["Scale"][oldVertIndex];

            var thisSpeed = WebGraphics.makePoint3D(
                this.attributeBuffers["Speed"][oldVertIndex * 3],
                this.attributeBuffers["Speed"][oldVertIndex * 3 + 1],
                this.attributeBuffers["Speed"][oldVertIndex * 3 + 2]
            );

            var thisColor = WebGraphics.makePoint4D(
                this.attributeBuffers["Color"][oldVertIndex * 4],
                this.attributeBuffers["Color"][oldVertIndex * 4 + 1],
                this.attributeBuffers["Color"][oldVertIndex * 4 + 2],
                this.attributeBuffers["Color"][oldVertIndex * 4 + 3]
            );

            var vertMin = WebGraphics.makePoint(
                this.attributeBuffers["Position"][oldVertIndex * 2],
                this.attributeBuffers["Position"][oldVertIndex * 2 + 1]
            );

            var vertMax = WebGraphics.makePoint(
                this.attributeBuffers["Position"][oldVertIndex * 2 + 4],
                this.attributeBuffers["Position"][oldVertIndex * 2 + 5]
            );

            var vertRect = TSDRectWithPoints(vertMin, vertMax);

            // set vertRect to empty if the particle from the object system is not visible
            if (this.objectSystem.particleSystemVisibilities[i] === false) {
                vertRect = TSDRectWithPoints(WebGraphics.makePoint(0, 0), WebGraphics.makePoint(0, 0));
            }

            var center = WebGraphics.makePoint(
                this.attributeBuffers["Center"][oldVertIndex * 2],
                this.attributeBuffers["Center"][oldVertIndex * 2 + 1]
            );

            var lifeSpan = WebGraphics.makePoint(
                this.attributeBuffers["LifeSpan"][oldVertIndex * 2],
                this.attributeBuffers["LifeSpan"][oldVertIndex * 2 + 1]
            );

            var p_particleHullArray = this.p_particleHullArray;
            var kNumHullPoints = this.kNumHullPoints;

            for (var v = 0; v < kNumHullPoints; ++v) {
                var newVertIndex = i * kNumHullPoints + v;

                var thisHullPoint = p_particleHullArray[v];

                var newVertexPoint = this.p_hullPoint(thisHullPoint, vertRect);
                KNWebGLUtil.setPoint2DAtIndexForAttribute(newVertexPoint, newVertIndex, newAttributeBuffers["Position"]);

                var newTexCoord = this.p_hullPoint(thisHullPoint, texRect);
                KNWebGLUtil.setPoint2DAtIndexForAttribute(newTexCoord, newVertIndex, newAttributeBuffers["ParticleTexCoord"]);

                KNWebGLUtil.setPoint2DAtIndexForAttribute(center, newVertIndex,  newAttributeBuffers["Center"]);
                KNWebGLUtil.setPoint4DAtIndexForAttribute(thisColor, newVertIndex, newAttributeBuffers["Color"]);
                KNWebGLUtil.setPoint3DAtIndexForAttribute(thisSpeed, newVertIndex, newAttributeBuffers["Speed"]);
                KNWebGLUtil.setFloatAtIndexForAttribute(thisScale, newVertIndex, newAttributeBuffers["Scale"]);
                KNWebGLUtil.setPoint2DAtIndexForAttribute(lifeSpan, newVertIndex, newAttributeBuffers["LifeSpan"]);
            }
        }

        // update data buffer with new data buffer
        this.attributeBuffers = newAttributeBuffers;

        // update element array
        var elementArray = this.elementArray = [];

        var indexCounter = 0;

        for (var i = 0; i < newParticleCount; i++) {
            elementArray[indexCounter++] = 4 * i + 0;
            elementArray[indexCounter++] = 4 * i + 1;
            elementArray[indexCounter++] = 4 * i + 2;

            // second triangle
            elementArray[indexCounter++] = 4 * i + 0;
            elementArray[indexCounter++] = 4 * i + 2;
            elementArray[indexCounter++] = 4 * i + 3;
        }
    },

    p_hullPoint: function(hullPoint, vertexRect) {
        var point = WebGraphics.makePoint(vertexRect.origin.x + vertexRect.size.width * hullPoint.x, vertexRect.origin.y + vertexRect.size.height * hullPoint.y);

        return point;
    },

    startingPointAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint) * 4;
        var objectSystemVertexCount = this.objectSystemVertexCount;
        var attributeBuffers = this.objectSystem.attributeBuffers["Position"];

        if (index < objectSystemVertexCount) {
            // Copy value from object particle system so this sparkle exactly matches up with an existing object particle
            var result = WebGraphics.makePoint(
                attributeBuffers[index * 2],
                attributeBuffers[index * 2 + 1]
            );

            return result;
        }

        // else, it's an extra sparkle at the end, in a random location!
        var halfWidth = this.objectSize.width / 2;
        var halfHeight = this.objectSize.height / 2;
        var midPoint = CGPointMake(halfWidth, halfHeight);

        // attenuate points towards the middle
        var r = WebGraphics.randomBetween(0, 1);
        var angle = WebGraphics.doubleBetween(0, 2.0 * Math.PI);
        var randLoc = CGPointMake(midPoint.x + halfWidth * r * Math.cos(angle), midPoint.y + halfHeight * r * Math.sin(angle));

        return randLoc;
    },

    speedAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint) * 4;
        var objectSystemVertexCount = this.objectSystemVertexCount;
        var attributeBuffers = this.objectSystem.attributeBuffers["Speed"];

        if (index < objectSystemVertexCount) {
            // Copy value from object particle system so this sparkle exactly matches up with an existing object particle
            var result = WebGraphics.makePoint3D(
                attributeBuffers[index * 3],
                attributeBuffers[index * 3 + 1],
                attributeBuffers[index * 3 + 2]
            );

            return result;
        }

        // else, it's an extra sparkle at the end; stay in place
        var speed = WebGraphics.makePoint3D(0, 0, 0);

        return speed;
    },

    scaleAtIndexPoint: function(indexPoint) {
        var minScale = 1.0;
        var maxScale = 25.0;
        var randNum = WebGraphics.randomBetween(0, 1);
        // Most particles will be smaller
        var result = TSUMix(minScale, maxScale, randNum * randNum);

        return result;
    },

    lifeSpanAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint) * 4;
        var objectSystemVertexCount = this.objectSystemVertexCount;

        if (index < objectSystemVertexCount) {
            // This is an existing object
            return WebGraphics.makePoint(0, 1);
        }

        // else, it's an extra sparkle at the end, with a random life span!

        // 2 second or 90% of total duration, whichever is shorter
        var lifeDuration = Math.min(2.0 / (this.duration / 1000), 0.9);
        var lifeStart = TSDMixFloats(0.01, 0.99, 1.0 - TSUReverseSquare(WebGraphics.randomBetween(0, 1)));

        // Since percent is non-uniform, make duration less at later part of anim
        lifeDuration *= TSUReverseSquare(lifeStart);

        // Make sure lifespan ends before animation completes
        lifeStart = Math.min(lifeStart, 0.99 - lifeDuration);

        var result = WebGraphics.makePoint(lifeStart, lifeDuration);
        return result;
    },

    colorAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint) * 4;
        var objectSystemVertexCount = this.objectSystemVertexCount;
        var attributeBuffers = this.objectSystem.attributeBuffers["Color"];

        if (index < objectSystemVertexCount) {
            // Copy value from object particle system so this sparkle exactly matches up with an existing object particle
            var result = WebGraphics.makePoint4D(
                attributeBuffers[index * 4],
                attributeBuffers[index * 4 + 1],
                attributeBuffers[index * 4 + 2],
                attributeBuffers[index * 4 + 3]
            );

            return result;
        }

        // else it's an extra sparkle at the end

        // white
        var result = WebGraphics.makePoint4D(1, 1, 1, 1);

        return result;
    },

    drawGLSLWithPercent: function(percent, opacity, rotation, clockwise) {
        // Rotation Matrix
        var angle = percent * (clockwise ? 1 : -1) * 2;
        var rotMatrix = CGAffineTransformMakeRotation(angle);
        var mat3 = WebGraphics.makeMat3WithAffineTransform(rotMatrix);

        // set mat3 uniform for RotationMatrix
        this.gl.uniformMatrix3fv(this.uniforms[kShimmerUniformRotationMatrix], false, mat3);

        // Particle Scale Percent
        var invPercent = 1.0 - percent;
        var powPercent = Math.pow(invPercent, 15.0);
        var particleScalePercent = TSUMix(invPercent * invPercent, 25. * percent, powPercent);

        this.gl.uniform1f(this.uniforms[kShimmerUniformParticleScalePercent], particleScalePercent);

        // Draw object
        this.drawFrame(percent, opacity);
    }
});

var KNWebGLBuildSparkleSystem = Class.create(KNWebGLParticleSystem, {
    initialize: function($super, renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture, direction) {
        this.willOverrideStartingPoints = true;

        // number of vertices per particle
        this.numberOfVerticesPerParticle = 4;

        // set direction
        this.direction = direction;

        $super(renderer, program, objectSize, slideSize, duration, particleSystemSize, particleSize, texture);

        // cache p_globalScale for faster access
        this.cachedGlobalScale = this.p_globalScale();

        this.setupWithTexture();
    },

    setupWithTexture: function() {
        this.animationWillBeginWithContext();
    },

    p_globalScale: function() {
        var objectSize = this.objectSize;
        var slideSize = this.slideSize;
        var minSide = Math.min(objectSize.width, objectSize.height);
        var minSideRatio = minSide / (Math.min(slideSize.width, slideSize.height));

        minSide = minSide / Math.sqrt(Math.sqrt(minSideRatio)) * 0.25;

        return minSide;
    },

    startingPointAtIndexPoint: function(indexPoint) {
        // CONSTANTS
        var maxOffset = 0.1 / (this.duration / 1000);

        var yAxis = (this.direction == KNDirection.kKNDirectionTopToBottom || this.direction == KNDirection.kKNDirectionBottomToTop);
        var reverse = (this.direction == KNDirection.kKNDirectionRightToLeft || this.direction == KNDirection.kKNDirectionTopToBottom);

        var index = this.indexFromPoint(indexPoint);

        var x = index / this.particleCount; // position along line
        x = reverse ? 1 - x : x;

        var axis1 = x + maxOffset * WebGraphics.doubleBetween(-1.0, 1.0);
        var axis2 = Math.random();
        // skew position towards center vertically
        axis2 = 2. * axis2 - 1.;
        axis2 *= Math.abs(axis2);
        axis2 = (axis2 + 1.) / 2.;

        var newX = yAxis ? axis2 : axis1;
        var newY = yAxis ? axis1 : axis2;

        var position = WebGraphics.makePoint(
            newX * this.objectSize.width - this.particleSize.width / 2.0,
            newY * this.objectSize.height - this.particleSize.height / 2.0
        );

        return position;
    },

    speedAtIndexPoint: function(point) {
        var speed = this.point3DRandomDirection();
        // reduce z speed
        speed.z *= 0.01;
        var randomMultiplier = Math.random();
        var result = WebGraphics.multiplyPoint3DByScalar(speed, randomMultiplier);

        return result;
    },

    speedMax: function() {
        var minSide = Math.min(this.objectSize.width, this.objectSize.height);
        var minSideRatio = minSide / (Math.min(this.slideSize.width, this.slideSize.height));

        minSide = minSide / Math.pow(minSideRatio, 0.667) * 0.25 * 1.5;

        return minSide;
    },

    scaleAtIndexPoint: function(indexPoint) {
        var minSide = this.cachedGlobalScale;
        var result = minSide / this.particleSize.width;

        return result;
    },

    lifeSpanAtIndexPoint: function(indexPoint) {
        var index = this.indexFromPoint(indexPoint);
        var timeStart = index / this.particleCount;
        var timeDuration = KNSparkleMaxParticleLife / Math.max(0.75, this.duration/1000);
        timeStart *= 1 - timeDuration;
        var lifeSpan = WebGraphics.makePoint(timeStart, timeDuration);

        return lifeSpan;
    }
});
